﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Debugger.Interop;
using Red.CommUtils;
using Red.CommUtils.Application;
using Red.Core.Network.PacketNetwork;
using Red.Scripts.Debugger.Defines;

namespace Red.Scripts.Debugger.Remote
{
    public class UriProcess : IDebugProcess2, IDebugProcessSecurity2
    {
        #region Public Constants

        public const int DEFAULT_PORT = 37060;
        public const string SCRIPTS_CHANNEL = "scripts";

        #endregion Public Constants

        #region Private Data

        private readonly Uri _uri;

        #endregion Private Data

        #region Public Properties

        public IDebugPort2 Parent { get; private set; }

        // Process info
        public string Filepath { get; private set; }
        public string Filename { get; private set; }
        public string Title { get; private set; }
        public string Username{ get; private set; }
        public int ProcessId { get; private set; }

        private ChannelFactory ChannelFactory { get; set; }

        #endregion Public Properties

        #region C-Tor

        public UriProcess( Uri uri, IDebugPort2 parentPort )
        {
            _uri = uri;
            Parent = parentPort;
        }

        public UriProcess( Uri uri, IDebugPort2 parentPort, ChannelFactory factory, int processId )
        {
            _uri = uri;
            Parent = parentPort;
            ChannelFactory = factory;
            ProcessId = processId;
        }

        #endregion C-Tor

        #region Public Methods

        public IChannel CreateChannel()
        {
            return AsyncHelpers.RunSync( async () => { return await CreateChannel( ChannelFactory, _uri ); } );
        }

        public static IEnumerable<UriProcess> PollForProcesses( IDebugPort2 parent, ChannelFactory factory, Uri uri, int portRange = 10 )
        {
            return PollForProcesses( parent, factory, uri, uri.IsDefaultPort ? DEFAULT_PORT : uri.Port, portRange );
        }

        public static IEnumerable<UriProcess> PollForProcesses( IDebugPort2 parent, ChannelFactory factory, Uri uri, int initialPort, int portRange = 10 )
        {
            return AsyncHelpers.RunSync( () => PollPortRangeForProcesses( parent, factory, uri, initialPort, portRange ) );
        }

        #endregion Public Methods

        #region Private Methods

        private static async Task<IEnumerable<UriProcess>> PollPortRangeForProcesses( IDebugPort2 parent, ChannelFactory factory, Uri uri, int initialPort, int portRange )
        {
            Task<UriProcess>[] channelTasks = new Task<UriProcess>[ portRange ];
            UriBuilder uriBuilder = new UriBuilder( uri );

            for ( int i = 0; i < portRange; ++i )
            {
                uriBuilder.Port = initialPort + i;

                channelTasks[ i ] = CreateProcess( factory, uriBuilder.Uri, parent );
            }

            await Task.WhenAll( channelTasks );

            return from task in channelTasks where task.Result != null select task.Result;
        }

        private static async Task<IChannel> CreateChannel( ChannelFactory factory, Uri uri )
        {
            TcpAddress address = new TcpAddress( uri );

            return await factory.TryOpenChannel( SCRIPTS_CHANNEL, address, 0, 0 );
        }

        private static async Task<UriProcess> CreateProcess( ChannelFactory factory, Uri uri, IDebugPort2 parent )
        {
            UriProcess process = null;

            IChannel channel = await CreateChannel( factory, uri );
            
            if( channel != null )
            {
                comm.ScriptProcessInfoRequest request = new comm.ScriptProcessInfoRequest();

                try
                {
                    const int timeout = 5000;
                    comm.ScriptProcessInfo response = channel.SendAndWait( request, timeout ) as comm.ScriptProcessInfo;

                    if ( response != null )
                    {
                        process = new UriProcess( uri, parent )
                        {
                            ProcessId = response.processId,
                            Filepath = response.exe,
                            Filename = Path.GetFileName( response.exe ),
                            Title = response.name,
                            Username = response.user,
                            ChannelFactory = factory
                        };
                    }
                }
                catch ( CommunicationException )
                {
                }

                channel.ShutDown();
            }

            return process;
        }

        #endregion Private Methods

        #region IDebugProcess2

        public int Attach( IDebugEventCallback2 pCallback, Guid[] rgguidSpecificEngines, uint celtSpecificEngines, int[] rghrEngineAttach )
        {
            throw new NotImplementedException();
        }

        public int CanDetach()
        {
            return Constants.S_OK;
        }

        public int CauseBreak()
        {
            return Constants.S_OK;
        }

        public int Detach()
        {
            throw new NotImplementedException();
        }

        public int EnumPrograms( out IEnumDebugPrograms2 ppEnum )
        {
            EnumeratedPrograms enumeration = new EnumeratedPrograms();
            enumeration.Elements.Add( new Program( this ) );

            ppEnum = enumeration;

            return Constants.S_OK;
        }

        public int EnumThreads( out IEnumDebugThreads2 ppEnum )
        {
            throw new NotImplementedException();
        }

        public int GetAttachedSessionName( out string pbstrSessionName )
        {
            throw new NotImplementedException();
        }

        public int GetInfo( enum_PROCESS_INFO_FIELDS Fields, PROCESS_INFO[] pProcessInfo )
        {
            var pi = new PROCESS_INFO();

            pi.Fields = Fields | enum_PROCESS_INFO_FIELDS.PIF_PROCESS_ID;
            pi.bstrFileName = Filepath;
            pi.bstrBaseName = Filename;
            pi.bstrTitle = Title;
            pi.ProcessId.dwProcessId = (uint)ProcessId;
            pi.dwSessionId = (uint)_uri.Port;

            pProcessInfo[ 0 ] = pi;

            return Constants.S_OK;

            // PIF_FILE_NAME = 1,
            // PIF_BASE_NAME = 2,
            // PIF_TITLE = 4,
            // PIF_PROCESS_ID = 8,
            // PIF_SESSION_ID = 16,
            // PIF_ATTACHED_SESSION_NAME = 32,
            // PIF_CREATION_TIME = 64,
            // PIF_FLAGS = 128,
            // PIF_ALL = 255
        }

        public int GetName( enum_GETNAME_TYPE gnType, out string pbstrName )
        {
            switch ( gnType )
            {
                case enum_GETNAME_TYPE.GN_TITLE:
                case enum_GETNAME_TYPE.GN_NAME:
                    pbstrName = Title;
                    break;

                case enum_GETNAME_TYPE.GN_FILENAME:
                    pbstrName = Filepath;
                    break;

                case enum_GETNAME_TYPE.GN_BASENAME:
                    pbstrName = Filename;
                    break;

                case enum_GETNAME_TYPE.GN_URL:
                    pbstrName = _uri.ToString();
                    break;

                // GN_MONIKERNAME = 3,
                // GN_STARTPAGEURL = 6

                default:
                    pbstrName = null;
                    break;
            }

            return Constants.S_OK;
        }

        public int GetPhysicalProcessId( AD_PROCESS_ID[] pProcessId )
        {
            pProcessId[ 0 ].ProcessIdType = (uint)enum_AD_PROCESS_ID.AD_PROCESS_ID_SYSTEM;
            pProcessId[ 0 ].dwProcessId = (uint)ProcessId;

            return Constants.S_OK;
        }

        public int GetPort( out IDebugPort2 ppPort )
        {
            ppPort = Parent;

            return Constants.S_OK;
        }

        public int GetProcessId( out Guid pguidProcessId )
        {
            pguidProcessId = Guid.Empty;

            return Constants.S_OK;
        }

        public int GetServer( out IDebugCoreServer2 ppServer )
        {
            throw new NotImplementedException();
        }

        public int Terminate()
        {
            throw new NotImplementedException();
        }

        #endregion IDebugProcess2

        #region IDebugProcessSecurity2

        public int QueryCanSafelyAttach()
        {
            return Constants.S_OK;
        }

        public int GetUserName( out string pbstrUserName )
        {
            pbstrUserName = Username;

            return Constants.S_OK;
        }

        #endregion IDebugProcessSecurity2
    }
}
